package gbench.appdemo.priceline.calendar;

import java.text.SimpleDateFormat;
import java.time.DayOfWeek;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.temporal.WeekFields;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;

import gbench.common.fs.FileSystem;
import gbench.common.tree.LittleTree.IRecord;
import gbench.common.tree.LittleTree.Tuple2;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.ToString;
import static gbench.common.tree.LittleTree.*;
import static gbench.common.tree.LittleTree.CronTime.*;
import static gbench.common.tree.LittleTree.IRecord.REC;

@Data
public class TradingCalendar implements ITradingCalendar{
    
    /**
     * 交易日历
     * @param ticker 期货代码
     * @param startDate　期货开始时间
     * @param endDate　期货交易截至时间包含．
     */
    public TradingCalendar(String ticker,LocalDate startDate,LocalDate endDate) {
        this.ticker = ticker;
        this.startDate = startDate;
        this.endDate = endDate;
    }
    
    /**
     * 注意不同日历(ticker,一个ticker一个交易日历) 下计算的索引index是不一样的
     *  根据指定的时间，生成交易所以所在的时间框索引
     *  索引时段从１开始．
     * @param time　交易时间
     * @param period　交易周期，交易周期，按照毫秒为单位．
     * @return时间框索引
     */
    @Override
    public  long getIndex(Date time,long period){
        return getIndex(dt2ldt(time),period);
    }
    
    public static WeekFields weekFields = WeekFields.of(DayOfWeek.SUNDAY,7);
    /**
     *  根据指定的时间，生成交易所以所在的时间框索引
     *  索引时段从１开始．
     * @param time　交易时间
     * @param period　交易周期，交易周期，按照毫秒为单位．
     * @return时间框索引
     */
    @Override
    public synchronized long getIndex(LocalDateTime  time,long period){
        synchronized(this.getClass()){
            final var ld = time.toLocalDate();
            if(period==PERIOD_UNIT_DAY) {// 日期索引
                var atom = new AtomicBoolean(false);
                var cnt =  this.getTradingDaysStream().map(e->e.date.compareTo(ld))
                .takeWhile(e->{var t = e<=0;if(e==0)atom.set(true);return t;}).count();
                return atom.get()?cnt:0;
            }else if(period==PERIOD_UNIT_WEEK){//索引
                var cnt = this.getTradingDaysStream()
                    .takeWhile(o->{var e=o.date.compareTo(ld);return e<=0;})
                    .map(e->{
                        var d = e.getDate();
                        var s = d.getYear()+""+d.get(weekFields.weekOfYear());
                        //System.out.println(d+":"+s);
                        return s;
                    }).distinct().count();
                return cnt;
            }else if(period==PERIOD_UNIT_MONTH) {
                var cnt = this.getTradingDaysStream()
                .takeWhile(o->{var e=o.date.compareTo(ld);return e<=0;})
                .map(e->{
                    var d = e.getDate();
                    return d.getYear()+""+d.getMonth().getValue();
                }).distinct().count();
                return cnt;
            }else if(period==PERIOD_UNIT_YEAR) {
                var cnt= time.getYear()-this.getStartDate().getYear()+1;
                return cnt;
            }else {// 默认连续时间处理
                long life = this.getLifeTime(time);//总的时间长度
                double d = life *1.0/period;// 计算分页
                double d2 = Math.floor(d+1);// +1 保证索引数值index从１开始．比如０,59按照６０取模都是　为１，取整数
                long d3 = (long)d2;//类型转换为目标类型．
                return (long) d3;
            }//
        }//synchronized
    }

    /**
     * 判断一个时刻是否位于交易时段
     * @param date
     */
    public boolean isTradingTime(Date date) {
        LocalDateTime ldt = date2localDateTime(date);
        LocalTime lt = ldt.toLocalTime();
        LocalDate ld = ldt.toLocalDate();
        var b = this.getGeneralTradingTimeSpan().parallelStream()
                .anyMatch(e->{
                    var _b =  lt.compareTo(e.getStartTime())>=0  &&
                                     lt.compareTo(e.getEndTime())<=0;// 交易时段条件
                    //debug_log(MFT("{0},{1}",e,_b));
                    return _b;});// 寻找是否可以存在一个位于交易时段．
        if(b) {
            debug_log("交易时段匹配");
            var b2=this.getTradingDaysStream().anyMatch(e->ld.equals(e.date));
            if(b2) {
                debug_log("交易日期匹配");
                return true;
            }else {
                debug_log("交易日期不匹配");
                return false;
            }
        }else {
            debug_log("交易时段不匹配");
            return false;
        }
    }
    
    /**
     * 交易时段
     * @author xuqinghua
     *
     */
    @Data @AllArgsConstructor
    public static class TimeSpan{
        String name;
        LocalTime startTime;
        LocalTime endTime;
        public String toString() {
            return name;
        }
        public long millis() {
            return Duration.between(this.startTime, this.endTime).toMillis();
        }
    }
    
    /**
     * 交易日:
     * 一个交易日分为一组交易时段
     * @author xuqinghua
     *
     */
    @Data @AllArgsConstructor @ToString
    public  static class TradingDay{
        /**
         * 交易日
         * @param date
         */
        public TradingDay(LocalDate date) {
            this.date = date;
        }
        
        private LocalDate date;// 交易日期
        private List<TimeSpan> spans;//  交易时段
    }
    
    /**
     * 
     */
    public String toString() {
        StringBuffer buffer = new StringBuffer();
        buffer.append("ticker:"+ticker+"\n");
        buffer.append(MFT("- start:{0},end:{1}\n",startDate,endDate));
        generalTradingTimeSpan.forEach(span->{
            buffer.append(MFT("-- general span:name:{0},start:{1},end:{2}\n",
                    span.name,span.startTime,span.endTime));
        });
        buffer.append(this.getTradingDaysList().stream().map(e->"---"+e.date
                +"\t"+e.getDate().getDayOfWeek()
                +"\t"+e.spans
                .stream().map(g->g.startTime+"->"+g.endTime).collect(Collectors.joining(" , "))
                ).collect(Collectors.joining("\n")));
        buffer.append("\n-- excludes:"+this.excludes);
        return buffer.toString();
    }

    /**
     * 交易日期间检测函数
     * 除掉：周六，周日
     */
    Predicate<LocalDate> predicate_trading_day = ( e)->!(
            e.getDayOfWeek()== DayOfWeek.SUNDAY ||
            e.getDayOfWeek()== DayOfWeek.SATURDAY||
            this.excludes.contains(e)
    );
    
    /**
     * 交易日
     * @param filter　交易日过滤器
     * @return
     */
    public List<TradingDay> getTradingDaysList(){
        return getTradingDaysList(predicate_trading_day);
    }
    
    /**
     * 交易日列表
     * @param filter　交易日过滤器
     * @return
     */
    public List<TradingDay> getTradingDaysList(Predicate<LocalDate>filter){
        return this.getTradingDaysStream(filter).collect(Collectors.toList());
    }
    
    /**
     * 交易日的数据流
     * @param trading_day_filter　交易日过滤器
     * @return　交易日流
     */
    public Stream<TradingDay> getTradingDaysStream(Predicate<LocalDate>trading_day_filter){
        return Stream.iterate(0,i->i+1) // 日期序列
                .map(startDate::plusDays) // 转换成日期
                .filter(trading_day_filter)// 把非交易时间给过滤掉
                .takeWhile(e->e.compareTo(endDate)<=0) // 获取到截至日期．包括截至日期．
                .map(e-> new TradingDay(e,this.generalTradingTimeSpan));
    }
    
    /**
     * 获得有效交易日列表串流
     * @param trading_day_filter　交易日过滤器
     * @return　交易日流
     */
    public Stream<TradingDay> getTradingDaysStream(){
        return getTradingDaysStream(this.predicate_trading_day);
    }
    
    /**
     * 获取交易日期历的启时间．
     * @return
     */
    public LocalDateTime getStartTradingTime() {
        LocalDateTime ldt  = LocalDateTime.of(this.startDate,this.getGeneralTradingTimeSpan().get(0).getStartTime());
        return ldt;
    }
    
    /**
     * 获取交易日期历的启时间．
     * @return
     */
    public LocalDateTime getEndTradingTime() {
        var spans = this.getGeneralTradingTimeSpan();
        var n = spans.size();
        LocalDateTime ldt  = LocalDateTime.of(this.endDate,spans.get(n-1).getEndTime());
        return ldt;
    }
    
    /**
     * 获取当前时刻：moment　距离开始时点之间之间的毫秒　时间长度．精确到毫秒
     * @param moment 时刻节点
     * @return　毫秒时长
     */
    public Long getLifeTime(Date moment) {
        return getLifeTime(date2localDateTime(moment));
    }
    
    private  long tradingspan_1day_total_time;//单个交易日的单位总共交易时间长度,用作缓存．
    private CalCache<LocalDate,Long> ndays_cache = new CalCache<>();// lifeTime 的按日期计的结果缓存
    
    /**
     * 获取当前时刻：moment　距离开始时点之间之间的毫秒　时间长度．精确到毫秒
     * @param ldt_moment　时刻信息
     * @return　一个时间长度，不是时刻
     */
    public Long getLifeTime(LocalDateTime ldt_moment) {
        LocalDateTime ldt_nearest_datetime = this.getNearestTradingTimeSpan(ldt_moment);
        if(ldt_nearest_datetime==null) {debug_log(MFT("{0}最近交易时段为空",ldt_moment));return -1l;}
        var lt_moment = ldt_nearest_datetime.toLocalTime();
        if(timeSpanLifeCache==null)this.initTimeSpanLifeCache();
        var timeSpanLife = this.timeSpanLifeCache.get(lt_moment);
        //计算与最近的交易时段之间毫秒差值
        var d = Duration.between(ldt_nearest_datetime.toLocalTime(), 
                ldt_moment.toLocalTime());// 时段之间的差额
        var lifeInTradingDay =timeSpanLife+d.toMillis();// 交易日之内的时长
        debug_log(MFT("{0}时段的当日时长:{1}　s",ldt_moment,lifeInTradingDay));
        
        var ld_start_date = this.getStartTradingTime().toLocalDate();//获取期货代码的最初交易时间．
        var ldt_nearest_date = ldt_nearest_datetime.toLocalDate();// 距离最近日的日期
        var ndays = ndays_cache.get(ldt_nearest_date, ldtnereastdate->this.getTradingDaysStream()
                    .map(e->e.date.compareTo(ldtnereastdate)<0?1:0) // 不包括最后一天
                    .takeWhile(e->e>0).count()/*累计交易日的个数．*/ );
        // 计算两个交易时间之间的日期差额
        debug_log(MFT("两个交易日之间的交易日个数:{0}-->{1}  共计：{2} 交易日（当前时刻所在交易日不算）",
                ld_start_date,ldt_nearest_date,ndays));
        
        var sum = tradingspan_1day_total_time!=0
                ?   tradingspan_1day_total_time // 也已计算则直接返回
                :   (tradingspan_1day_total_time=this.getGeneralTradingTimeSpan().stream()
                    .map(e->diff_millis_secs(e.getStartTime(),e.getEndTime()))// 交易时段的时间长度
                    .collect(Collectors.summarizingLong(e->e)).getSum());// 交易日的单位总共交易时间长度
        
        var lifetime= lifeInTradingDay+sum*ndays;
        debug_log(MFT("单个交易日生命时间长度{0}ms",sum));
        debug_log(MFT("交易日生命时间长度({0}*{1}={2})+{3}={4}ms",ndays,sum,
                ndays*sum,lifeInTradingDay,lifetime));
        return lifetime;
    }
    
    /**
     * 获取当前距离当前时刻moment，最近的交易开始时段．
     * ldt只有为　交易时间的的分段：[startTime,endTime)之内部才算有效，截止时间不包含，开始时间包含．
     * @param date　时刻信息
     * @return
     */
    public LocalDateTime getNearestTradingTimeSpan(Date moment) {
        return this.getNearestTradingTimeSpan(date2localDateTime(moment));
    }

    /**
     * 派出在外的日期
     * @param ld　派出在外的日期
     */
    public void addExclude(LocalDate ld) {
        this.excludes.add(ld);
    }

    /**
     * 派出在外的日期
     * @param ld　派出在外的日期
     */
    public void addSpan(TimeSpan span) {
        this.generalTradingTimeSpan.add(span);
    }

    private  CalCache<LocalDate,LocalDate> nearest_date_cache = new CalCache<>();
    
    /**
     *      这是定位当前交易时段的分区． 获取任意一个时间的最近的交易时段．
     * ldt只有为　交易时间的的分段：[startTime,endTime)之内部才算有效，截止时间不包含，开始时间包含．
     * @param ldt 任意一个时数值,null 返回null
     * @return　当前的交易日与交易时间．
     */
    public LocalDateTime getNearestTradingTimeSpan(LocalDateTime ldt) {
        if(ldt==null)return null;
        LocalTime lt = ldt.toLocalTime();// 获取时段
        var opt = this.getGeneralTradingTimeSpan().stream()
            .dropWhile(span->lt.compareTo(span.getEndTime())>0) // 去除晚于时段结束时间出现的交易时段.截止时间不包含
            .filter(span->lt.compareTo(span.getStartTime())>=0) //找出晚于交易时段开始时刻的交易时段．开始时间包含
            .findFirst();// 找到第一个
        
        if(opt.isEmpty()) {debug_log(MFT("{0}交易时间不在任何有效时段",ldt));return null;}
        else {debug_log(MFT("{0}最近的时段{1}",ldt,opt.get().getStartTime()));}
        
        LocalDate ld = ldt.toLocalDate();// 本地日期
        // 检索最近一个交易日
        var d= nearest_date_cache.get(ld, localdate->{
            var opt_ld = getTradingDaysStream().dropWhile(e->localdate.compareTo(e.getDate())!=0).findFirst();
            if(opt_ld.isEmpty()) {debug_log(MFT("{0}时间尚未处于交易日",ld));
                return TradingCalendar.this.startDate;}// 只能返回首个交易日
            else {debug_log("最近的交易日期："+opt_ld.get().getDate());}
            return opt_ld.get().getDate();
        });
        
        // 返回最近的交易日：交易时段
        return LocalDateTime.of(d, opt.get().getStartTime());
    }
    
    /**
     * 初始化TimeSpanLifeCache
     */
    public  void initTimeSpanLifeCache() {
        var s =0l;
        timeSpanLifeCache = new HashMap<>();
        for(var ts:this.generalTradingTimeSpan) {
            this.timeSpanLifeCache.put(ts.startTime, s);
            s+=ts.millis();
        }
    }

    /**
     * 计算两个日期之天数
     * @param start
     * @param end
     * @return
     */
    public static long diff_days(LocalDate start,LocalDate end) {
        return end.toEpochDay()-start.toEpochDay();
    }

    /*
     * 计算两个时段信息之间的毫秒差:lt2-lt1
     */
    static long  diff_millis_secs(LocalTime lt1,LocalTime lt2) {
        return Duration.between(lt1, lt2).toMillis();//  转换成毫秒
    }

    /**
     *   分析一个日期字符串
     * @return　日期非法返回null
     */
    public static Date parseDate(String line) {
        var sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date d = null;
        try {d = sdf.parse(line);} catch (Exception e) {e.printStackTrace();}
        return d;
    }

    /**
     * 调试信息输出
     * @param line
     */
    public static void debug_log(Object line) {
        if(debug)System.out.println(line.toString());
    }
    
    /**
     * 把字符串转换成数字
     * @param digits　数字字符串
     * @return
     */
    public static int i4(String digits) {
        return Integer.parseInt(digits);
    }
    
    /**
     * 股票交易期限设计
     * @param file　期货信息文件
     */
    public static Map<String,TradingCalendar> parse(String file) {
        Pattern headerPattern = Pattern.compile(// 期货表头
                "\\s*ticker\\s*:\\s*([^\\s,]+)[\\s,]+((\\d{2,4})\\s*-\\s*(\\d{1,2})\\s*-\\s*(\\d{1,2}))[,\\s]+((\\d{2,4})\\s*-\\s*(\\d{1,2})\\s*-\\s*(\\d{1,2}))\\s*");
         Pattern spanPattern = Pattern.compile(// 期货表体
                    "\\s*trading-span\\s*\\(([^\\)]+)\\)\\s*:\\s*((\\d{1,2})\\s*:\\s*(\\d{1,2}))[\\s-]*((\\d{2})\\s*:\\s*(\\d{1,2}))\\s*");
         Pattern exceptPattern = Pattern.compile("\\s*exclude-days\\s*:\\s*([^\\s]+)");// 例外日期
        LinkedList<Tuple2<String,TradingCalendar>> ll = new LinkedList<>();
        // 文件解析
        FileSystem.localUtf8Lines(file, TradingCalendar.class)
            .filter(line->{
                return ! (  line.matches("\\s*") ||// 空行
                            line.matches("^\\s*([#]+|-{2,}).*$")// 以#　-２个以上　或者　＃开头的行
                );// 过滤掉注释项目与空白项目
            })
            .map(e->{
                return e.replaceAll("\\s*--\\s*[^-]*$", "");
            })
            .forEach(line->{
                var hm = headerPattern.matcher(line); 
                if(hm.matches()) {//开始日期
                    String ticker = hm.group(1);// 期货名称
                    LocalDate lstart = LocalDate.of(i4(hm.group(3)),i4(hm.group(4)),i4(hm.group(5)));
                    LocalDate lend= LocalDate.of(i4(hm.group(7)),i4(hm.group(8)),i4(hm.group(9)));
                    var calendar = new TradingCalendar(ticker,lstart,lend);
                    ll.add(new Tuple2<>(ticker,calendar));
                    //debug_log(MFT("ticker:{0},start:{1},end:{2}",ticker,lstart,lend));
                }else{// 交易日：
                    var sm = spanPattern.matcher(line);//交易时段的选择
                    if(sm.matches()) {
                        var calendar = ll.getLast()._2();//获取当前的交易日历
                        String name = sm.group(1);// 时段名称
                        int start_hour = i4(sm.group(3)); int start_min = i4(sm.group(4));
                        LocalTime startTime=LocalTime.of(start_hour, start_min);
                        int end_hour = i4(sm.group(6)); int end_min = i4(sm.group(7));
                        LocalTime endTime=LocalTime.of(end_hour, end_min);
                        var span = new TimeSpan(name,startTime,endTime);
                        calendar.addSpan(span);
                        //debug_log(MFT("span:{0},start:{1},end:{2}",span,startTime,endTime));
                    }else {//去除日期：例外日期的选择
                        var em = exceptPattern.matcher(line);
                        if(em.matches()) {
                            Pattern datePattern =Pattern.compile("(\\d{2,4})\\s*-\\s*(\\d{1,2})\\s*-\\s*(\\d{1,2})");
                            Matcher dm = datePattern.matcher(em.group(1));
                            while(dm.find()) {
                                var year = i4(dm.group(1));
                                if(year<100)year+=2000;// 调整日期为４位年
                                var mon = i4(dm.group(2));
                                var day = i4(dm.group(3));
                                LocalDate d= LocalDate.of(year, mon, day);
                                var calendar = ll.getLast()._2();//获取当前的交易日历
                                calendar.addExclude(d);
                            }//while
                            //debug_log(line);
                        }//em.matches
                    }//例外日期
                }//if
            });// forEach line
        
        Map<String,TradingCalendar> map= new HashMap<>();
        ll.forEach(tup->{
            tup._2().initTimeSpanLifeCache();// 初始timespanLife
            map.put(tup._1(), tup._2());
        });
        return map;
    }
    
    /**
     * 
     * @author xuqinghua
     *
     * @param <K>
     * @param <V>
     */
    public class CalCache<K,V> {
        
        /**
         * 
         * @param key
         * @param callable
         * @return
         */
        public V get(K key,Function<K,V>foo){
            V v = GOOG_CACHE.getIfPresent(key);
            if(v==null) {v = foo.apply(key);}
            //else {System.out.println("cache命中！");}
            GOOG_CACHE.put(key, v);
            return v;
        }
        
        private Cache<K,V> GOOG_CACHE = CacheBuilder.newBuilder()
            .initialCapacity(10) //设置cache的初始大小为10，要合理设置该值
            .concurrencyLevel(5) //设置并发数为5，即同一时间最多只能有5个线程往cache执行写入操作
            .maximumSize(5000) // 设置最大缓存数量
            .expireAfterWrite(50,TimeUnit.SECONDS) //设置cache中的数据在写入之后的存活时间为300秒,5分钟 
            .build(); //构建cache实例
    }
    
    /**
     * 把一个IRecord记录转换成Calendar结构。  <br>
     *   // 交易日历 <br>
     *   var rec = REC( <br>
     *       "name","MA001","ticker","MA001","description","MA001", // name:日历名称,ticker:合约代码,description:日历说明 <br>
     *       "start",ld("2019-01-01"),"end",ld("2019-12-31"), // 开始时间:包含,结束时间:不包含 <br>
     *       "excludes",IRecord.L("2019-01-01,2019-01-05,2019-09-13,2019-09-30,2019-10-01,2019-10-02,2019-10-03,2019-10-04".split(",")) <br>
     *           .stream().map(e->ld(e)).collect(Collectors.toList()), // 排除在外的日期 <br>
     *       "timespans",IRecord.L(// 时间段:单日内的时间跨度 <br>
     *               R("name","上午-前","start",lt("09:00"),"end",lt("10:15")), <br>
     *               R("name","上午-后","start",lt("10:30"),"end",lt("11:30")), <br>
     *               R("name","下午","start",lt("13:30"),"end",lt("15:00")), <br>
     *               R("name","夜间","start",lt("21:00"),"end",lt("23:00")) <br>
     *        ),// timespan <br>
     *       "createtime",now() <br>
     *   );// 交易日历的定义
     * 
     * @param r 一个日历记录结构 见上图示例
     * @return SimpleTradingCalendar
     */
    public static TradingCalendar rec2calendar(IRecord r) {
        var calendar = new TradingCalendar(r.str("ticker"),
            r.ld("start"),r.ld("end"));
        //非法日期
        r.lla("excludes",e->REC("_k",e).ld("_k")).forEach(calendar::addExclude);
        //交易时段
        r.lla("timespans",e->(IRecord)e).stream().map(e->
            new TimeSpan(e.str("name"),e.lt("start"),e.lt("end")))
        .forEach(calendar::addSpan);
        return calendar;
    }
    
    private String ticker;// 期货代码
    private LocalDate startDate;// 交易开始日
    private LocalDate endDate;// 交易结束日期
    private List<TimeSpan> generalTradingTimeSpan = new ArrayList<>();// 一般交易时段:[{9:00,1:30),{10:45,11:00}]
    private List<LocalDate> excludes = new LinkedList<>();
    private Map<LocalTime,Long> timeSpanLifeCache = null;
    public static boolean debug = false;// 调试标记
    public static final long PERIOD_UNIT_SEC=1l* 1000;
    public static final long PERIOD_UNIT_MIN=1l* 60 * 1000;
    public static final long PERIOD_UNIT_5MIN=5l * 60 * 1000;
    public static final long PERIOD_UNIT_10MIN=10l * 60 * 1000;
    public static final long PERIOD_UNIT_15MIN=15l * 60 * 1000;
    public static final long PERIOD_UNIT_30MIN=30l * 60 * 1000;
    public static final long PERIOD_UNIT_HOUR=60l * 60 * 1000;
    public static final long PERIOD_UNIT_DAY=1l * 86400 * 1000;
    public static final long PERIOD_UNIT_WEEK=7l * 86400 * 1000;
    public static final long PERIOD_UNIT_MONTH=30l * 86400 * 1000;
    public static final long PERIOD_UNIT_YEAR=365l * 86400 * 1000;
    public boolean enableDWMY=true;// 是否开启日月年
}

